﻿using Academy.Numerics.Properties;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Academy.Numerics
{
    /// <summary>
    /// Provides a base class for implementations of the <see cref="T:Academy.Numerics.IArithmeticOperator"/> generic interface.
    /// </summary>
    public abstract class ArithmeticOperator : IArithmeticOperator
    {
        #region Private Region
        private static readonly OperatorInfo AdditionInfo = new OperatorInfo { OpChar = '+', MtString = "Add", OpString = "op_Addition", DynamicOp = AddDynamically };
        private static readonly OperatorInfo SubstractionInfo = new OperatorInfo { OpChar = '-', MtString = "Subtract", OpString = "op_Substraction", DynamicOp = SubstractDynamically };
        private static readonly OperatorInfo MultiplyInfo = new OperatorInfo { OpChar = '*', MtString = "Multiply", OpString = "op_Multiply", DynamicOp = MultiplyDynamically };
        private static readonly OperatorInfo DivisionInfo = new OperatorInfo { OpChar = '/', MtString = "Divide", OpString = "op_Division", DynamicOp = DivideDynamically };
        private static readonly OperatorInfo ModulusInfo = new OperatorInfo { OpChar = '%', MtString = "Mod", OpString = "op_Modulus", DynamicOp = ModDynamically };
        private const int lowNumericRange = 5;
        private const int uppNumericRange = 15;
        private delegate object DynamicOperation(object obj1, object obj2);
        private static readonly string arithOperableName = typeof(IArithmeticallyOperable).FullName;
        private static readonly string convertibleName = typeof(IConvertible).FullName;
        private static volatile ArithmeticOperator defaultOperator;

        private struct OperatorInfo
        {
            internal char OpChar;
            internal string OpString;
            internal string MtString;
            internal DynamicOperation DynamicOp;
        }
        private static object AddDynamically(object obj1, object obj2)
        {
            return (dynamic)obj1 + (dynamic)obj2;
        }
        private static object SubstractDynamically(object obj1, object obj2)
        {
            return (dynamic)obj1 - (dynamic)obj2;
        }
        private static object MultiplyDynamically(object obj1, object obj2)
        {
            return (dynamic)obj1 * (dynamic)obj2;
        }
        private static object DivideDynamically(object obj1, object obj2)
        {
            return (dynamic)obj1 / (dynamic)obj2;
        }
        private static object ModDynamically(object obj1, object obj2)
        {
            return (dynamic)obj1 % (dynamic)obj2;
        }
        #endregion

        /// <summary>
        /// Gets a default arithmetic operator for objects.
        /// </summary>
        public static ArithmeticOperator Default
        {
            get
            {
                ArithmeticOperator def = defaultOperator;
                if (def == null)
                {
                    def = new DefaultOperator();
                    defaultOperator = def;
                }
                return defaultOperator;
            }
        }

        /// <summary>
        /// When overridden in a derived class, gets the result of adding the two specified values.
        /// </summary>
        /// <param name="left">The left operand for the addition operation.</param>
        /// <param name="right">The right operand for the addition operation.</param>
        /// <returns>The result of adding the two specified values.</returns>
        public abstract object Add(object left, object right);
        /// <summary>
        /// When overridden in a derived class, gets the result of subtract the two specified values.
        /// </summary>
        /// <param name="left">The left operand. Represents the minuend in the operation.</param>
        /// <param name="right">The right operand. Represents the subtrahend in the operation.</param>
        /// <returns>The result of substract the left operand from the right operand.</returns>
        public abstract object Subtract(object left, object right);
        /// <summary>
        /// When overridden in a derived class, gets the result of multiply the two specified values.
        /// </summary>
        /// <param name="left">The left operand. Represents the multiplicand in the operation.</param>
        /// <param name="right">The right operand. Represents the multiplier in the operation.</param>
        /// <returns>The result of multiply the left operand by the right operand.</returns>
        public abstract object Multiply(object left, object right);
        /// <summary>
        /// When overridden in a derived class, gets the result of divide the two specified values.
        /// </summary>
        /// <param name="left">The left operand. Represents the dividend in the operation.</param>
        /// <param name="right">The right operand. Represents the divisor in the operation.</param>
        /// <returns>The result of divide the left operand by the right operand.</returns>
        public abstract object Divide(object left, object right);
        /// <summary>
        /// When overridden in a derived class, gets the remainder value of divide the two specified values.
        /// </summary>
        /// <param name="left">he left operand. Represents the dividend in the operation.</param>
        /// <param name="right">The right operand. Represents the divisor in the operation.</param>
        /// <returns>The remainder value of divide the left operand by the right operand.</returns>
        public abstract object Mod(object left, object right);

        private class DefaultOperator : ArithmeticOperator
        {
            public override object Add(object left, object right)
            {
                return Invoke(AdditionInfo, left, right);
            }
            public override object Subtract(object left, object right)
            {
                return Invoke(SubstractionInfo, left, right);
            }
            public override object Multiply(object left, object right)
            {
                return Invoke(MultiplyInfo, left, right);
            }
            public override object Divide(object left, object right)
            {
                return Invoke(DivisionInfo, left, right);
            }
            public override object Mod(object left, object right)
            {
                return Invoke(ModulusInfo, left, right);
            }
            private object Invoke(OperatorInfo info, object obj1, object obj2)
            {
                Type typeObj1 = obj1.GetType();
                Type typeObj2 = obj1.GetType();
                IArithmeticallyOperable opObj = obj1 as IArithmeticallyOperable;
                if (opObj != null)
                {
                    MethodInfo op2 = typeObj1.GetMethod(info.MtString);
                    return op2.Invoke(obj1, new object[] { obj2 });
                }
                if (typeObj1.GetInterface(convertibleName) != null)
                {
                    IConvertible covertible = obj1 as IConvertible;
                    int code = (int)covertible.GetTypeCode();
                    if ((code >= lowNumericRange) && (code <= uppNumericRange))
                    {
                        try
                        {
                            object result = info.DynamicOp(obj1, obj2);
                            return result;
                        }
                        catch
                        {
                            Thrower.InvalidOperationException(GetExceptionString(info, typeObj1));
                        }
                    }
                }
                MethodInfo op = typeObj1.GetMethod(info.OpString);
                if (op == null)
                {
                    Thrower.InvalidOperationException(GetExceptionString(info, typeObj1));
                }
                return op.Invoke(null, new object[] { obj1, obj2 });
            }
            private string GetExceptionString(OperatorInfo info, Type objType)
            {
                return string.Format(Resources.InvalidOperation_NotOperableOrOperatorDefined_G, objType.FullName, arithOperableName, info.OpChar);
            }
        }
    }
}